import Ext from '@/common/web_extension';
import storage from '@/common/ext_storage_local';
import { postWindowMessage, onWindowMessage } from '@/apps/content_script/cs_postmessage';
import inspector from '@/common/inspector';
import * as CONSTANT from '@/common/constant';
import {
    pick,
    setIn,
    updateIn,
    until,
    parseQuery,
    objMap,
    bindOnce,
    bind,
    withTimeout,
    subjectiveBindOnce
} from '@/common/utils';
import {
    bindContentEditableChange,
    setStyle,
    domText,
    getElementByLocator
} from '@/common/dom_utils';
import { hackAlertInject } from './eval';
import { run } from './command_runner';
import { captureClientAPI } from '@/common/capture_screenshot'; // 截图
import { encryptIfNeeded } from '@/common/encrypt'; // 加密
import log from '@/common/log'; // 日志
import { selectAreaPromise } from './select_area'; // 选择区域
import { getOrcMatchesHighlighter } from '@/services/ocr/highlighter';
import { DevicePixelRatioService } from '@/services/dpr';
import { ViewportRectService } from '@/services/viewport_rect';
import config from '@/config';
import { getState, updateState } from '@/common/ext_storage_state';
import { askBg } from '@/common/ipc/ipc';
console.log('content_script.js loaded:>>');

// 2s后，遮罩自动消失
const MASK_CLICK_FADE_TIMEOUT = 2000;
// develop环境，打印错误日志
const oops = process.env.NODE_ENV === 'production' ? () => { } : (e: any) => log.error(e.stack);
// 状态
const state: any = {
    status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL,
    // Note: it decides whether we're running commands
    // in the current window or some iframe/frame
    playingFrame: window,
    // Note: current frame stack when recording, it helps
    // to generate `selectFrame` commands
    recordingFrameStack: [],
    // Note: snapshot of extension config (content script cares about click/clickAt when recording)
    // It is supposed to be updated when user activates that page
    config: {},
    // Note: To achieve verifyText, we need contextmenu event on page plus menu item click event from background
    elementOnContextMenu: null,
};

// Note: Whether it's top or inner window, a content script has the need
// to send IPC message to background. But in our design, only the top window
// has access to the real csIpc, while inner frames have to bubble up the messages
// to the top window.
// So inner windows are provided with a fake csIpc, which post messages to its parent
// 顶层发送IPC消息
const superAskBg = window.top === window
    ? askBg
    : (ipcAction: string, ipcData: any) => {
        return postWindowMessage(window.parent, window, {
            action: 'IPC_CALL',
            data: { ipcAction, ipcData },
        });
    }
// 计算选择帧命令
const calcSelectFrameCmds = (frameStack: any) => {
    let xs = state.recordingFrameStack;
    let ys = frameStack;
    let len = Math.min(xs.length, ys.length);
    let tpl = { cmd: 'selectFrame', url: window.location.href };
    let ret = [];
    let i = 0;

    for (i = 0; i < len; i++) {
        if (xs[i] !== ys[i]) {
            break;
        }
    }

    if (i === 0) {
        // No need for relative=top, if state.recordingFrameStack is empty
        if (xs.length !== 0) {
            ret.push({ ...tpl, target: 'relative=top' });
        }
    } else if (i < len) {
        for (let j = i; j < xs.length; j++) {
            ret.push({ ...tpl, target: 'relative=parent' });
        }
    }

    for (let j = i; j < ys.length; j++) {
        ret.push({ ...tpl, target: ys[j] });
    }

    return ret;
};

// Two masks to show on page
// 1. mask on click
// 2. mask on hover
// 点击和hover的遮罩
const getMask = (function () {
    let mask: any, factory: any;

    const addLogoImg = ($dom: any) => {
        const $img = createLogoImg();

        inspector.setStyle($img, {
            position: 'absolute',
            top: '-25px',
            left: '0',
            width: '20px',
            height: '20px',
        });

        $dom.appendChild($img);
    };

    return (remove?: boolean) => {
        if (remove && factory) {
            mask = null;
            return factory.clear();
        }

        if (mask) {
            return mask;
        }

        factory = inspector.maskFactory();

        const maskClick = factory.gen({ background: 'green', border: '2px solid purple' });
        const maskHover = factory.gen({ background: '#ffa800', border: '2px solid purple' });

        addLogoImg(maskClick);
        addLogoImg(maskHover);

        mask = { maskClick, maskHover };

        document.documentElement.appendChild(maskClick);
        document.documentElement.appendChild(maskHover);

        return mask;
    };
})();
// 创建logo图
const createLogoImg = () => {
    // Note: Ext.runtime.getURL is available in content script, but not injected js
    // So there are cases when content_script.js is run as injected js, where `Ext.runtime.getURL`
    // is not available
    // Weird enough, `Ext.runtime.getURL` always works well in macOS
    const url = Ext.runtime.getURL ? Ext.runtime.getURL('logo.png') : '';
    const img = new Image();

    img.src = url;
    return img;
};
// 命令添加等待
const addWaitInCommand = (cmdObj: any) => {
    const { cmd } = cmdObj;

    switch (cmd) {
        case 'click':
        case 'clickAt':
            return { ...cmdObj, cmd: 'clickAndWait', value: '' };

        case 'select':
            return { ...cmdObj, cmd: 'selectAndWait' };

        default:
            return cmdObj;
    }
};

// report recorded commands to background.
// transform `leave` event to clickAndWait / selectAndWait event based on the last command
// 录制命令
const reportCommand = (function () {
    const LEAVE_INTERVAL = 500;
    let last: any = null;
    let lastTime: any = null;
    let timer: any = null;

    return (obj: any) => {
        obj = { ...obj, url: window.location.href };

        log('to report', obj);

        // Change back to top frame if it was recording inside
        if (state.recordingFrameStack.length > 0) {
            state.recordingFrameStack = [];

            superAskBg('CS_RECORD_ADD_COMMAND', {
                cmd: 'selectFrame',
                target: 'relative=top',
                url: window.location.href,
            }).catch(oops);
        }

        switch (obj.cmd) {
            case 'leave': {
                if (timer) {
                    clearTimeout(timer);
                }

                if (new Date().getTime() - lastTime < LEAVE_INTERVAL) {
                    obj = addWaitInCommand(last);
                } else {
                    return;
                }

                break;
            }
            case 'click':
            case 'clickAt':
            case 'select': {
                timer = setTimeout(() => {
                    superAskBg('CS_RECORD_ADD_COMMAND', obj).catch(oops);
                }, LEAVE_INTERVAL);

                last = obj;
                lastTime = new Date();

                return;
            }

            default:
                break;
        }

        last = obj;
        lastTime = new Date();

        superAskBg('CS_RECORD_ADD_COMMAND', obj).catch(oops);
    };
})();

// 是否为选择框
const isValidSelect = (el: any) => {
    const tag = el.tagName.toLowerCase();
    if (['option', 'select'].indexOf(tag) !== -1) return true;
    return false;
};
// 是否为输入框
const isValidType = (el: any) => {
    const tag = el.tagName.toLowerCase();
    const type = el.getAttribute('type');

    if (tag === 'textarea') return true;
    if (tag === 'input' && ['radio, checkbox'].indexOf(type) === -1) return true;

    return false;
};

// 下载图片
const downloadSaveItem = ($dom: any, cmd: any) => {
    if (cmd == 'saveItem') {
        let cachedImage = $dom;
        let url = cachedImage.src;
        let filename = url.substring(url.lastIndexOf('/') + 1);

        // Check if the cached image exists
        // 缓存存在
        if (cachedImage.complete && cachedImage.naturalWidth !== 0) {
            //  从缓存中下载
            // If the cached image exists, create a temporary URL for the image
            const tempUrl = URL.createObjectURL(cachedImage.src);
            const link = document.createElement('a');
            link.download = filename;
            link.href = tempUrl;
            link.click();
            // Release the temporary URL
            URL.revokeObjectURL(tempUrl);
        } else {
            // If the cached image doesn't exist, create a new image object
            const image = new Image();
            // Set the image source URL
            image.src = url;
            // Wait for the image to load
            image.onload = () => {
                // Store the image in cache for future use
                const canvas = document.createElement('canvas');
                canvas.width = image.naturalWidth;
                canvas.height = image.naturalHeight;
                canvas.getContext('2d')!.drawImage(image, 0, 0);
                const extension = filename.split('.').pop();
                const dataURL = canvas.toDataURL(`image/${extension}`);
                localStorage.setItem('myImage', dataURL);
                // Create a temporary URL for the image
                const tempUrl = URL.createObjectURL(image.src as any);
                // Create a link element with a download attribute
                const link = document.createElement('a');
                link.download = filename;
                link.href = tempUrl;
                // Click the link to trigger a download of the image
                link.click();
                // Release the temporary URL
                URL.revokeObjectURL(tempUrl);
            };
        }
    }
};

// 回放时，高亮显示dom，3s后自动消失
const highlightDom = ($dom: any, timeout: any) => {
    const mask: any = getMask();

    inspector.showMaskOver(mask.maskClick, $dom);

    setTimeout(() => {
        inspector.setStyle(mask.maskClick, { display: 'none' });
    }, timeout || MASK_CLICK_FADE_TIMEOUT);
};
// 创建高亮图标
const createHighlightX = function (opts: any = {}) {
    const $mask = document.createElement('div');

    $mask.innerHTML = `
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="16px" height="16px">
      <line x1="8" y1="8" x2="56" y2="56" stroke="red" stroke-width="12"></line>
      <line x1="56" y1="8" x2="8" y2="56" stroke="red" stroke-width="12"></line>
    </svg>
                `;
    let timer: any;

    inspector.setStyle($mask, {
        position: 'absolute',
        zIndex: 110001,
        display: 'none',
        pointerEvents: 'none',
    });

    return (rect: any, timeout: any) => {
        clearTimeout(timer);

        inspector.setStyle($mask, {
            display: 'block',
            top: `${rect.top - 8}px`,
            left: `${rect.left - 8}px`,
        });

        if (!$mask.parentNode) {
            document.documentElement.appendChild($mask);
        }

        if (opts.scrollIntoView) {
            $mask.scrollIntoView({ block: 'center', behavior: 'smooth' });
        }

        if (timeout && timeout > 0) {
            timer = setTimeout(() => {
                inspector.setStyle($mask, { display: 'none' });
            }, timeout);
        }

        const fn = () => {
            inspector.setStyle($mask, { display: 'none' });
            $mask.remove();
        };

        Object.assign(fn, {
            hide: () => inspector.setStyle($mask, { display: 'none' }),
            show: () => inspector.setStyle($mask, { display: 'block' }),
        });

        return fn;
    };
};
// 高亮图标
const highlightX = createHighlightX();
// 创建高亮框
const createHighlightRect = function (opts: any = {}) {
    const $mask = document.createElement('div');
    const $text = document.createElement('div');
    let timer: any;

    inspector.setStyle($mask, {
        position: 'absolute',
        zIndex: 110001,
        border: '1px solid orange',
        color: 'orange',
        display: 'none',
        pointerEvents: 'none',
    });

    inspector.setStyle($text, {
        position: 'absolute',
        top: 0,
        left: 0,
        transform: 'translate(-100%, -100%)',
        fontSize: '14px',
    });

    $mask.appendChild($text);

    return (rect: any, timeout: any) => {
        clearTimeout(timer);
        $text.innerText = rect.text ? rect.text : parseFloat(rect.score).toFixed(2) + ` #${rect.index + 1}`;

        inspector.setStyle($mask, {
            display: 'block',
            top: `${rect.top}px`,
            left: `${rect.left}px`,
            width: `${rect.width}px`,
            height: `${rect.height}px`,
            ...(opts.rectStyle || {}),
        });

        inspector.setStyle($text, opts.textStyle || {});

        if (!$mask.parentNode) {
            document.documentElement.appendChild($mask);
        }

        if (opts.scrollIntoView) {
            $mask.scrollIntoView({ block: 'center', behavior: 'smooth' });
        }

        if (timeout && timeout > 0) {
            timer = setTimeout(() => {
                inspector.setStyle($mask, { display: 'none' });
            }, timeout);
        }

        const fn = () => {
            inspector.setStyle($mask, { display: 'none' });
            $mask.remove();
        };

        Object.assign(fn, {
            hide: () => inspector.setStyle($mask, { display: 'none' }),
            show: () => inspector.setStyle($mask, { display: 'block' }),
        });

        return fn;
    };
};
// 高亮一个区域
const highlightRect = createHighlightRect();
// 高亮多个区域
const highlightRects: any = (function () {
    const topMatchedOptions = {
        rectStyle: {
            borderColor: '#ff0000',
            color: '#ff0000',
        },
    };
    let destroy: any;

    const fn = (rects: any, timeout?: any) => {
        if (destroy) destroy();

        const destroyFns = rects.map((rect: any, i: number) => {
            rect.index = i;
            return createHighlightRect(rect.selected ? topMatchedOptions : {})(rect, timeout);
        });

        destroy = () => destroyFns.forEach((destroy: any) => destroy());

        Object.assign(destroy, {
            hide: () => destroyFns.forEach((fn: any) => fn.hide && fn.hide()),
            show: () => destroyFns.forEach((fn: any) => fn.show && fn.show()),
        });

        return destroy;
    };

    Object.assign(fn, {
        hide: () => destroy.hide(),
        show: () => destroy.show(),
    });

    return fn;
})();
// 多选
const initMultipleSelect = ($select: any) => {
    if ($select && $select.nodeName && $select.nodeName.toLowerCase() === 'select' && $select.multiple) {
        Array.from($select.options).forEach(($option: any) => {
            $option.lastSelected = $option.selected;
        });
    }
};
// 右键菜单
const onContextMenu = (e: any) => {
    state.elementOnContextMenu = e.target;
};
// 点击时录制，如果是选项，初始化选择框
const onClick = (e: any) => {

    if (!state.config) return
    (async () => {
        const allState = await getState();
        if (allState['curent_cmd'] === 'XClickTextRelative' && allState.status !== 'NORMAL') {
            const rect = e.target.getBoundingClientRect();
            getOrcMatchesHighlighter().highlightRelative(rect);
            updateState(setIn(['curent_cmd'], ''));
            //e.target.style.border='2px solid #fe1492';
        }
    })();
    const targetInfo = inspector.getLocator(e.target, true);

    log('onClick, switch  case', state.config.recordClickType);
    switch (state.config.recordClickType) {
        case 'clickAt':
            reportCommand({
                ...targetInfo,
                cmd: 'clickAt',
                value: (function () {
                    const { clientX, clientY } = e;
                    const { top, left } = e.target.getBoundingClientRect();
                    const x = Math.round(clientX - left);
                    const y = Math.round(clientY - top);
                    return `${x},${y}`;
                })(),
            });
            break;
        case 'click':
        default:
            reportCommand({
                ...targetInfo,
                cmd: 'click',
            });
            break;
        case 'saveItem':
            console.log('saveItem==> ', targetInfo);
            break;
    }

    if (e.target.nodeName.toLowerCase() === 'option') {
        initMultipleSelect(e.target.parentNode);
    }
};
// 获得焦点时，初始化选择框
const onFocus = (e: any) => {
    if (e.target.nodeName.toLowerCase() === 'select' && e.target.multiple) {
        initMultipleSelect(e.target);
    }
};
// 值改变时，录制命令
const onChange = (e: any) => {
    if (isValidSelect(e.target)) {
        // 选择框

        const isMultipleSelect = !!e.target.multiple;

        if (!isMultipleSelect) {
            // 单选款，设置值
            const value = e.target.value;
            const $option = Array.from(e.target.children).find(($op: any) => $op.value === value);

            reportCommand({
                cmd: 'select',
                value: 'label=' + inspector.domText($option).trim(),
                ...inspector.getLocator(e.target, true),
            });
        } else {
            // 多选框，选择或移除选项
            Array.from(e.target.options).forEach(($option: any) => {
                if ($option.lastSelected !== $option.selected) {
                    reportCommand({
                        cmd: $option.selected ? 'addSelection' : 'removeSelection',
                        value: 'label=' + inspector.domText($option).trim(),
                        ...inspector.getLocator(e.target, true),
                    });
                }
                $option.lastSelected = $option.selected;
            });
        }
    } else if (isValidType(e.target)) {
        // 输入框
        const value = (e.target.value || '').replace(/\n/g, '\\n');
        // 加密
        encryptIfNeeded(value, e.target).then((realValue) => {
            reportCommand({
                cmd: 'type',
                value: realValue,
                ...inspector.getLocator(e.target, true),
            });
        });
    }
};
// 富文本框内容发生改变，录制命令
const onContentEditableChange = (e: any) => {
    reportCommand({
        cmd: 'editContent',
        value: e.target.innerHTML,
        ...inspector.getLocator(e.target, true),
    });
};
// 拖拽，录制命令
const onDragDrop = (function () {
    let dragStart: any = null;

    return (e: any) => {
        switch (e.type) {
            case 'dragstart': {
                dragStart = inspector.getLocator(e.target, true);
                break;
            }
            case 'drop': {
                if (!dragStart) return;
                const tmp = inspector.getLocator(e.target, true);
                const drop = {
                    value: tmp.target,
                    valueOptions: tmp.targetOptions,
                };

                reportCommand({
                    cmd: 'dragAndDropToObject',
                    ...dragStart,
                    ...drop,
                });

                dragStart = null;
            }
        }
    };
})();
// 页面关闭，录制命令
const onLeave = (e: any) => {
    reportCommand({
        cmd: 'leave',
        target: null,
        value: null,
    });

    setTimeout(() => {
        reportCommand({
            cmd: 'pullback',
            target: null,
            value: null,
        });
    }, 800);
};

// 录制时绑定的事件
let unbindContentEditableEvents: any;

// 绑定录制相关事件
const bindEventsToRecord = () => {
    document.addEventListener('click', onClick, true);
    document.addEventListener('focus', onFocus, true);
    document.addEventListener('change', onChange, true);
    document.addEventListener('dragstart', onDragDrop, true);
    document.addEventListener('drop', onDragDrop, true);
    document.addEventListener('contextmenu', onContextMenu, true);
    window.addEventListener('beforeunload', onLeave, true);

    unbindContentEditableEvents = bindContentEditableChange({ onChange: onContentEditableChange } as any);
};
// 卸载录制相关事件
const unbindEventsToRecord = () => {
    document.removeEventListener('click', onClick, true);
    document.removeEventListener('focus', onFocus, true);
    document.removeEventListener('change', onChange, true);
    document.removeEventListener('dragstart', onDragDrop, true);
    document.removeEventListener('drop', onDragDrop, true);
    document.removeEventListener('contextmenu', onContextMenu, true);
    window.removeEventListener('beforeunload', onLeave, true);

    if (unbindContentEditableEvents) {
        unbindContentEditableEvents();
    }
};

// 等待页面加载完成
const waitForDomReady = (accurate: any) => {
    return until(
        'dom ready',
        () => {
            return {
                pass: ['complete', 'interactive'].slice(0, accurate ? 1 : 2).indexOf(document.readyState) !== -1,
                result: true,
            };
        },
        1000,
        6000 * 10
    );
};
// 通知当前frame内部的所有frame进行状态的同步,一层层传递下去
const broadcastToAllFrames = (action: any, data: any) => {
    // IMPORTANT: broadcast status change to all frames inside
    const frames = window.frames;

    for (let i = 0, len = frames.length; i < len; i++) {
        postWindowMessage(frames[i], window, {
            action,
            data,
        });
    }
};
// 更新状态，所有frame中都存在状态
const updateStatus = (args: any) => {
    console.log('updateStatus:', args)
    if (!args.status) {
        throw new Error('SET_STATUS: 缺少参数 args.status');
    }
    if (!CONSTANT.CONTENT_SCRIPT_STATUS[args.status]) {
        throw new Error('SET_STATUS: 无效参数 args.status - ' + args.status);
    }

    Object.assign(state, {
        status: args.status,
    });

    if (args.status === CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING) {
        // 录制中，绑定事件
        bindEventsToRecord();
    } else {
        // 非录制中，卸载事件
        unbindEventsToRecord();
    }

    // replace alert/confirm/prompt with our version when playing
    // 执行中，替换alert/confirm/prompt，防止阻塞浏览器
    if (args.status === CONSTANT.CONTENT_SCRIPT_STATUS.PLAYING) {
        hackAlertConfirmPrompt();
    } else {
        restoreAlertConfirmPrompt();
    }

    // Note: clear recording frame stack whenever it stops recording
    // 停止录制，清空frame堆栈
    if (args.status === CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL) {
        state.recordingFrameStack = [];
        state.playingFrame = window;
    }

    // IMPORTANT: broadcast status change to all frames inside
    // 通知内部的frame同步状态
    broadcastToAllFrames('SET_STATUS', args);
};

// 监听IPC通信消息
const bindIPCListener = () => {
    // Note: need to check csIpc in case it's a none-src iframe into which we
    // inject content_script.js. It has no access to chrome api, thus no csIpc available

    // Note: csIpc instead of superIpc, because only top window is able
    // to listen to ipc events from bg

    const listener = (cmd: any, args: any) => {
        log(cmd, args);

        switch (cmd) {
            // 心跳
            case 'HEART_BEAT':
                return {
                    secret: null,
                };
            // 更新状态
            case 'SET_STATUS': {
                updateStatus(args);
                return true;
            }
            // 等待 DOM ready
            case 'DOM_READY':
                return waitForDomReady(false);
            // 停止捕获
            case 'STOP_INSPECTING': {
                getMask(true);
                updateStatus({ status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL });
                return true;
            }
            // 执行命令
            case 'RUN_COMMAND':
                return runCommand(args.command)
                    .catch((e: any) => {
                        // Mark that there is already at least one command run
                        (window as any).noCommandsYet = false;
                        log.error(e.stack);
                        throw e;
                    })
                    .then((data: any) => {
                        // Mark that there is already at least one command run
                        (window as any).noCommandsYet = false;

                        if (state.playingFrame !== window) {
                            return {
                                success: true,
                                data,
                                isIFrame: true
                            };
                        }

                        return {
                            success: true,
                            data
                        };
                    });
            // 获取元素
            case 'FIND_DOM': {
                try {
                    const $el = getElementByLocator(args.locator, false);
                    return true;
                } catch (e) {
                    return false;
                }
            }
            // 高亮元素
            case 'HIGHLIGHT_DOM': {
                const $el: any = getElementByLocator(args.locator, false);

                if ($el) {
                    $el.scrollIntoView({ block: 'center' });
                    highlightDom($el, 0);
                    downloadSaveItem($el, args.cmd);
                }

                return true;
            }
            // 显示矩形
            case 'HIGHLIGHT_RECT': {
                highlightRect(args.rect, args);
                return true;
            }
            // 显示 X
            case 'HIGHLIGHT_X': {
                const rect = {
                    top: args.offset.y,
                    left: args.offset.x,
                    // width: 40,
                    // height: 60
                };
                highlightX(rect, args);
                return true;
            }
            // 显示多个矩形
            case 'HIGHLIGHT_RECTS': {
                highlightRects(
                    args.scoredRects.map((rect: any, i: number) => {
                        return {
                            ...rect,
                            selected: i === args.selectedIndex,
                        };
                    })
                );
                return true;
            }
            // 清除矩形
            case 'CLEAR_VISION_RECTS': {
                // Note: it will clear previous rects
                highlightRects([]);
                return true;
            }
            // 隐藏矩形
            case 'HIDE_VISION_RECTS': {
                highlightRects.hide();
                return true;
            }
            // 显示矩形
            case 'SHOW_VISION_RECTS': {
                highlightRects.show();
                return true;
            }
            // 显示 OCR 匹配
            case 'HIGHLIGHT_OCR_MATCHES': {
                getOrcMatchesHighlighter().updateStates(args.localStorage);
                getOrcMatchesHighlighter().highlight(args.ocrMatches, args.showOcrOverlay);
                return true;
            }
            // 清除 OCR 匹配
            case 'CLEAR_OCR_MATCHES': {
                getOrcMatchesHighlighter().clear();
                return true;
            }
            // 拦截 alert
            case 'HACK_ALERT': {
                return hackAlertConfirmPrompt().then(
                    () => true,
                    () => true
                );
            }
            // 标记页面还没有执行过命令
            case 'MARK_NO_COMMANDS_YET': {
                (window as any).noCommandsYet = true;
                return true;
            }

            //  
            case 'SCREENSHOT_PAGE_INFO': {
                return captureClientAPI.getPageInfo();
            }

            //  
            case 'START_CAPTURE_FULL_SCREENSHOT': {
                return captureClientAPI.startCapture(args || {});
            }

            // 
            case 'END_CAPTURE_FULL_SCREENSHOT': {
                return captureClientAPI.endCapture(args.pageInfo);
            }

            // 滚动页面
            case 'SCROLL_PAGE': {
                return captureClientAPI.scrollPage(args.offset);
            }

            case 'TAB_ACTIVATED': {
                loadConfig();
                return;
            }

            case 'SELECT_SCREEN_AREA': {
                return selectAreaPromise({
                    done: (rect: any, boundingRect: any) => {
                        log('SELECT_SCREEN_AREA  - selectArea', rect, boundingRect);
                        return askBg('CS_SCREEN_AREA_SELECTED', {
                            rect: {
                                x: boundingRect.x,
                                y: boundingRect.y,
                                width: boundingRect.width,
                                height: boundingRect.height,
                            },
                            devicePixelRatio: window.devicePixelRatio,
                        });
                    },
                });
            }

            case 'TOGGLE_HIGHLIGHT_VIEWPORT': {
                const on = args.on;
                const id = '__kantu_viewport_highlight__';
                const $el = document.getElementById(id);

                if ($el) {
                    $el.remove();
                }

                if (on) {
                    const $dom = document.createElement('div');
                    $dom.id = id;
                    $dom.innerText = 'Calibrating Computer Vision...';

                    setStyle($dom, {
                        position: 'fixed',
                        top: 0,
                        bottom: 0,
                        left: 0,
                        right: 0,
                        zIndex: 120001,
                        background: '#00ff00',
                        color: '#fff',
                        fontSize: '18px',
                        fontWeight: 'bold',
                        display: 'flex',
                        flexDirection: 'row',
                        justifyContent: 'center',
                        alignItems: 'center',
                    });

                    document.documentElement.appendChild($dom);
                }

                return true;
            }

            case 'CONTEXT_MENU_IN_RECORDING': {
                switch (args && args.command) {
                    case 'verifyText':
                    case 'assertText':
                        if (!state.elementOnContextMenu) {
                            break;
                        }

                        reportCommand({
                            ...inspector.getLocator(state.elementOnContextMenu, true),
                            cmd: args.command,
                            value: domText(state.elementOnContextMenu),
                        });
                        break;

                    case 'verifyTitle':
                    case 'assertTitle':
                        reportCommand({
                            cmd: args.command,
                            target: document.title,
                        });
                        break;
                }

                return true;
            }

            case 'GET_VIEWPORT_RECT_IN_SCREEN': {
                const dprService = new DevicePixelRatioService({
                    getZoom: () => Promise.resolve(args.zoom),
                });
                const viewportRectService = new ViewportRectService({
                    devicePixelRatioService: dprService,
                });

                return viewportRectService.getViewportRectInScreen();
            }

            // 收到消息
            case 'INVOKE_RESPONSE': {
                invokeResponse(args);
                return true;
            }

            default:
                throw new Error('cmd not supported: ' + cmd);
        }
    }

    Ext.runtime.onMessage.addListener((req: any, sender: any, sendResponse: any) => {
        console.log(req, sender, 'content接收到消息')
        if (req.type === 'BG_ASK_CS') {
            Promise.resolve().then(() => listener(req.cmd, req.args)).then(res => {
                sendResponse(res)
            }).catch((e: any) => {
                console.error('BG_ASK_CS Error ==:>> ', e)
                sendResponse({
                    data: null,
                    success: false,
                    error: e.message
                })
            })
        }
        return true;// 异步
    })

};

// 监听拓展的storage发生改变，同步状态
const bindStorageListener = () => {
    Ext.storage.local.get().then((store: any) => {

        if (store.background_state.status === CONSTANT.APP_STATUS.NORMAL) {
            // 正常
            updateStatus({ status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL });
        } else if (store.background_state.status === CONSTANT.APP_STATUS.RECORDER) {
            // 录制中
            updateStatus({ status: CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING });
        }
    })

    // 监听status的改变
    Ext.storage.local.onChanged.addListener((store: any) => {
        if (store.background_state.oldValue.status === CONSTANT.APP_STATUS.RECORDER && store.background_state.newValue.status === CONSTANT.APP_STATUS.NORMAL) {
            // 停止录制
            updateStatus({ status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL });
        } else if (store.background_state.oldValue.status === CONSTANT.APP_STATUS.NORMAL && store.background_state.newValue.status === CONSTANT.APP_STATUS.RECORDER) {
            // 开始录制
            updateStatus({ status: CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING });
        }
    })
}
// 绑定捕获元素事件
const bindEventsToInspect = () => {

    // Bind click events for inspecting
    document.addEventListener(
        'click',
        (e) => {
            switch (state.status) {
                case CONSTANT.CONTENT_SCRIPT_STATUS.INSPECTING: {
                    e.preventDefault();
                    e.stopPropagation();

                    const mask = getMask();

                    inspector.setStyle(mask.maskHover, { display: 'none' });

                    inspector.showMaskOver(mask.maskClick, e.target);
                    setTimeout(() => {
                        inspector.setStyle(mask.maskClick, { display: 'none' });
                    }, MASK_CLICK_FADE_TIMEOUT);

                    Object.assign(state, {
                        status: CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL,
                    });

                    return superAskBg('CS_DONE_INSPECTING', {
                        locatorInfo: inspector.getLocator(e.target, true),
                    })
                        .catch(oops);
                }

                default:
                    break;
            }
        },
        true
    );

    // bind mouse over event for applying for a inspector role
    document.addEventListener('mouseover', (e) => {
        console.log('mouseover', state.status)
        if (state.status === CONSTANT.CONTENT_SCRIPT_STATUS.NORMAL) {
            return superAskBg('CS_ACTIVATE_ME', {}).catch(oops);
        }
    });

    // bind mouse move event to show hover mask in inspecting
    document.addEventListener('mousemove', (e) => {

        if (state.status === CONSTANT.CONTENT_SCRIPT_STATUS.INSPECTING || state.status === CONSTANT.CONTENT_SCRIPT_STATUS.RECORDING) {
            // 捕获或录制中 显示hover
            const mask = getMask();
            inspector.showMaskOver(mask.maskHover, e.target);
        } else {
            // 其他状态隐藏hover
            const mask = getMask();

            inspector.setStyle(mask.maskHover, { display: 'none' });

        }
    });
};
// 绑定监听window消息
const bindOnMessage = () => {
    onWindowMessage(window, ({ action, data }: any, { source }: any) => {
        // Should not log source here, because it might cause accessing a cross origin frame error
        log('onMessage', action, data);

        switch (action) {
            case 'SET_STATUS':
                updateStatus(data);
                return true;

            case 'UPDATE_CONFIG': {
                state.config = data || {};
                return true;
            }

            // inner frames may receive this message when there are
            // some previous `selectFrame` command
            case 'RUN_COMMAND':
                // runCommand will decide whether to run in this window or pass on
                return runCommand(data);

            // inner frames send IPC_CALL to background,
            // It will go step by step up to the topmost frame, which has
            // the access to csIpc
            case 'IPC_CALL':
                // When recording, need to calculate `selectFrame` by ourselves
                // * for inner frames, add current frame locator to frame stack
                // * for top frame, send `selectFrame` commands before the original command
                //   and keep track of the latest frame stack
                if (data.ipcAction === 'CS_RECORD_ADD_COMMAND' && data.ipcData.cmd !== 'pullback') {
                    // Note: Do not send any RECORD_ADD_COMMAND in playing mode
                    if (state.status === CONSTANT.CONTENT_SCRIPT_STATUS.PLAYING) {
                        return false;
                    }

                    data = updateIn(['ipcData', 'frameStack'], (stack = []) => [inspector.getFrameLocator(source, window), ...stack], data);

                    if (window.top === window) {
                        calcSelectFrameCmds(data.ipcData.frameStack).forEach((cmd) => {
                            askBg('CS_RECORD_ADD_COMMAND', cmd).catch(oops);
                        });

                        state.recordingFrameStack = data.ipcData.frameStack;
                    }
                }

                if (window.top === window) {
                    return askBg(data.ipcAction, data.ipcData).catch(oops);
                } else {
                    return postWindowMessage(window.parent, window, { action, data });
                }

            case 'RESET_PLAYING_FRAME': {
                state.playingFrame = window;

                // pass on RESET_PLAYING_FRAME to parent, all the way till top window
                if (data === 'TOP' && window.top !== window) {
                    return withTimeout(config.iframePostMessageTimeout, () => {
                        return postWindowMessage(window.parent, window, {
                            action: 'RESET_PLAYING_FRAME',
                            data: 'TOP',
                        });
                    }).then(() => true);
                }

                return Promise.resolve(true);
            }

            case 'SOURCE_PAGE_OFFSET': {
                const $frames = [...Array.from(document.getElementsByTagName('iframe')), ...Array.from(document.getElementsByTagName('frame'))];
                const $frameElement = $frames.find(($frame) => $frame.contentWindow === source);
                const offset = inspector.offset($frameElement, true);
                const x = offset.left;
                const y = offset.top;
                log('SOURCE_PAGE_OFFSET, iframeDOM', $frameElement);

                if (window.top === window) {
                    return { x, y };
                }

                return postWindowMessage(window.parent, window, {
                    action: 'SOURCE_PAGE_OFFSET',
                    data: {},
                }).then((parentOffset: any) => {
                    return {
                        x: x + parentOffset.x,
                        y: y + parentOffset.y,
                    };
                });
            }
            // 获取当前frame的offset
            case 'SOURCE_VIEWPORT_OFFSET': {
                const $frames = [...Array.from(document.getElementsByTagName('iframe')), ...Array.from(document.getElementsByTagName('frame'))];
                const $frameElement: any = $frames.find(($frame) => $frame.contentWindow === source);
                const rect = $frameElement.getBoundingClientRect();

                if (window.top === window) {
                    // 到顶了
                    return {
                        x: rect.x,
                        y: rect.y,
                    };
                }
                // 往上找累加
                return postWindowMessage(window.parent, window, {
                    action: 'SOURCE_VIEWPORT_OFFSET',
                    data: {},
                }).then((parentOffset: any) => {
                    return {
                        x: rect.x + parentOffset.x,
                        y: rect.y + parentOffset.y,
                    };
                });
            }
            // 等待document.readyState === complete
            case 'DOM_READY':
                return waitForDomReady(false);
        }
    });
};
// 是url否加入了白名单
const isUrlInWhiteList = (url: string) => {
    const { websiteWhiteList = [] } = state.config;

    return websiteWhiteList.reduce((prev: string, cur: string) => {
        if (prev) return prev;
        return url.indexOf(cur) === 0;
    }, false);
};

// 监听自定义事件，触发运行用例或导入并运行用例
const bindInvokeEvent = () => {
    const doesQueriesContainMacroOrTestSuite = (queries: any = {}) => {
        return queries['macro'] || queries['testsuite'] || queries['folder'];
    };
    const decorateOptions = (options: any, detail?: any) => {
        return {
            loadmacrotree: '0',
            continueInLastUsedTab: '1',
            ...options,
            ...(detail ? pick(['closeKantu', 'closeRPA', 'loadmacrotree'], detail) : {}),
        };
    };
    const runCsInvokeFromQueries = (queries: any = {}) => {
        const userStorageMode = queries.storageMode ? queries.storageMode.toLowerCase() : '';
        const isValidStorageMode = ['browser', 'xfile'].indexOf(userStorageMode) !== -1;
        const storageMode = isValidStorageMode ? userStorageMode : 'browser';

        if (queries['macro']) {
            return askBg('CS_INVOKE', {
                testCase: {
                    storageMode,
                    name: queries['macro'],
                    from: 'html',
                },
                options: decorateOptions(queries),
            })
                .catch((e: any) => alert('[RPA IDE] ' + e.message));
        }

        if (queries['folder']) {
            return askBg('CS_INVOKE', {
                testSuite: {
                    storageMode,
                    macroFolder: queries['folder'],
                    from: 'html',
                },
                options: decorateOptions(queries),
            });
        }

        if (queries['testsuite']) {
            return askBg('CS_INVOKE', {
                testSuite: {
                    storageMode,
                    name: queries['testsuite'],
                    from: 'html',
                },
                options: decorateOptions(queries),
            })
                .catch((e: any) => alert('[RPA IDE] ' + e.message));
        }
    };
    const isFile = window.location.protocol === 'file:' || window.location.href.includes('ui.vision.html');

    // Macros 监听自定义事件，触发执行用例
    bind(window, 'kantuRunMacro', (e: any) => {
        log('invoke event', e);
        window.dispatchEvent(new CustomEvent('kantuInvokeSuccess'));

        const queries = parseQuery(window.location.search);

        askBg('CS_INVOKE', {
            testCase: e.detail,
            options: decorateOptions(
                {
                    continueInLastUsedTab: '0',
                    ...queries,
                },
                e.detail
            ),
        })
            .catch((e: any) => alert('[RPA IDE] ' + e.message));
    });
    // 监听自定义事件，触发导入并运行用例
    (isFile ? bindOnce : subjectiveBindOnce)(window, 'kantuSaveAndRunMacro', (e: any) => {
        const run = () => {
            log('save and run macro event:>> e:', e);
            window.dispatchEvent(new CustomEvent('kantuInvokeSuccess'));

            if (!e.detail || (!e.detail.html && !e.detail.json)) {
                return alert('[RPA IDE] invalid data format');
            }

            const queries = parseQuery(window.location.search);

            // lw: 修改入口调用参数
            if (!queries.macro) {
                queries.macro = e.detail.macro;
            }
            if (!queries.folder) {
                queries.folder = e.detail.folder;
            }
            if (!queries.testsuite) {
                queries.folder = e.detail.testsuite;
            }
            if (!queries.direct) {
                queries.direct = e.detail.direct;
            }

            const direct = !!queries['direct'] || (e.detail.json && e.detail.direct);
            const storageMode = queries['storage'] || e.detail.storageMode || 'browser';

            const msgDirectParam = 'RPA IDE: Do you want to import and run this macro?\n\nNote: To remove this dialog, add \'?direct=1\' switch to the URL. Example: file:///xx/xx/macro.html?direct=1  For embedded macros, add "direct: true" to the call.';
            const msgWebsiteWhiteList = 'RPA IDE: Do you want to import and run this macro?\n\nNote: To remove this dialog, add this site to whitelist in the RPA IDE settings';

            if (isFile && !direct) {
                const agree = confirm(msgDirectParam);
                if (!agree) return;
            }

            if (!isFile) {
                if (!state.config?.allowRunFromHttpSchema) {
                    return alert('[Message from RPA IDE] Error #110: To run an embedded macro from a website, you need to allow it in the RPA settings first');
                }

                if (!isUrlInWhiteList(window.location.href)) {
                    const agree = confirm(msgWebsiteWhiteList);
                    if (!agree) return;
                } else if (!direct) {
                    const agree = confirm(msgDirectParam);
                    if (!agree) return;
                }
            }
            const extraOptions = !isFile ? { continueInLastUsedTab: '0' } : {};

            if (doesQueriesContainMacroOrTestSuite(queries)) {
                console.log('doesQueriesContainMacroOrTestSuite:>> queries: ', queries);
                return runCsInvokeFromQueries({ ...queries, ...extraOptions, storageMode });
            } else if (e.detail.noImport) {
                const msg = 'Command line must include one of these params: macro, folder, testsuite';
                alert(msg);
                throw new Error(msg);
            }

            return askBg('CS_IMPORT_AND_INVOKE', {
                ...e.detail,
                from: 'html',
                options: decorateOptions({ ...queries, ...extraOptions }, e.detail),
            })
                .catch((e: any) => alert('[RPA IDE] ' + e.message));
        };

        loadConfig()
            .catch((e) => { })
            .then(run);
    });
};
// 替换alert、confirm、prompt方法，防止浏览器阻塞
const hackAlertConfirmPrompt = () => {
    const script = `(function () {
    if (!window.oldAlert)     window.oldAlert   = window.alert
    if (!window.oldConfirm)   window.oldConfirm = window.confirm
    if (!window.oldPrompt)    window.oldPrompt  = window.prompt

    window.alert = function (str) {
      document.body.setAttribute('data-alert', str)
    }

    window.confirm = function (str) {
      document.body.setAttribute('data-confirm', str)
      return true
    }

    window.prompt = function (str) {
      let answer = document.body.getAttribute('data-prompt-answer')
      document.body.setAttribute('data-prompt', str)
      document.body.setAttribute('data-prompt-answer', '')
      return answer
    }
  })();`;

    return hackAlertInject(script);
};
// 还原alert、confirm、prompt方法
const restoreAlertConfirmPrompt = () => {
    const script = `(function () {
    if (window.oldAlert)    window.alert = window.oldAlert
    if (window.oldConfirm)  window.confirm = window.oldConfirm
    if (window.oldPrompt)   window.prompt = window.oldPrompt
  });`;

    return hackAlertInject(script);
};

// 添加或移除标识属性，用来判断是否安装了此插件
const toggleBodyMark = (shouldMark: boolean) => {
    const $root = document.documentElement;

    if (!$root) {
        return;
    }

    if (shouldMark) {
        $root.setAttribute('data-kantu', '1');
    } else {
        $root.removeAttribute('data-kantu');
    }
};
// 根据配置移除插件标志
const removeBodyMarkIfNecessary = (config: any) => {
    switch (window.location.protocol) {
        case 'file:':
            if (!config.allowRunFromFileSchema) {
                toggleBodyMark(false);
            }
            break;

        case 'http:':
        case 'https:':
            if (!config?.allowRunFromHttpSchema) {
                toggleBodyMark(false);
            }
            break;

        default:
            toggleBodyMark(false);
    }
};

// 执行命令
const runCommand = (command: any) => {
    console.log('runCommand command :>> ', command);
    if (!command.cmd) {
        throw new Error('runCommand: must provide cmd');
    }

    const pResult = (() => {
        console.log('pResult state :>> ', state);
        // if it's an 'open' command, it must be executed in the top window
        if (state.playingFrame === window || command.cmd === 'open') {
            // 必须在window中执行
            // Note: both top and inner frames could run commands here
            // So must use superAskBg instead of csIpc
            const ret = run(command, {
                highlightDom,
                hackAlertConfirmPrompt,
                xpath: inspector.xpath,
            });

            // Note: `run` returns the contentWindow of the selected frame
            if (command.cmd === 'selectFrame') {
                return ret.then(({ frame }: any) => {
                    const p = (() => {
                        // let outside window know that playingFrame has been changed, if it's parent or top
                        if (frame !== window && (frame === window.top || frame === window.parent)) {
                            // set playingFrame to own window, get ready for later commands if any
                            state.playingFrame = window;

                            return withTimeout(config.iframePostMessageTimeout, () => {
                                return postWindowMessage(window.parent, window, {
                                    action: 'RESET_PLAYING_FRAME',
                                    data: frame === window.top ? 'TOP' : 'PARENT',
                                });
                            });
                        } else {
                            state.playingFrame = frame;

                            return Promise.resolve();
                        }
                    })();

                    return p.then(() => ({
                        pageUrl: window.location.href,
                        extra: command.extra,
                    }));
                });
            }

            // Extra info passed on to background, it contains timeout info
            const wrapResult = (ret: any) => {
                return {
                    ...(typeof ret === 'object' ? ret : {}),
                    pageUrl: window.location.href,
                    extra: command.extra,
                    // Note: undefined value in an Object will be eliminated during message passing,
                    // Have to transform it into an object first, and convert it back in front end
                    vars: !ret.vars
                        ? undefined
                        : objMap((val) => {
                            return val !== undefined ? val : { __undefined__: true };
                        }, ret.vars),
                };
            };

            return Promise.resolve(ret).then(wrapResult);
        } else {
            const isFrameRemoved = (frame: any) => !frame.parent;

            if (isFrameRemoved(state.playingFrame)) {
                throw new Error('The selected frame has been removed. You may want to use another selectFrame before its removal');
            }

            // log('passing command to frame...', state.playingFrame, '...', window.location.href)
            // Note: pass on the command if our window is not the current playing one
            return postWindowMessage(state.playingFrame, window, {
                action: 'RUN_COMMAND',
                data: command,
            });
        }
    })();

    // 注意：在响应时设置ipc secret，这样后台就可以知道页面是刷新还是重定向了
    // Note: set ipc secret on response, so that background could know whether a page has refreshed or redirected
    // only mark it on top-most window
    return pResult.then((result: any) => {
        console.log('pResult result:>> ', result);
        const secret = result.secret ||
            (() => {
                if (window.top === window) {
                    return null;
                }

                (window as any).kantuSecret = (window as any).kantuSecret || '' + Math.floor(Math.random() * 10000);
                return (window as any).kantuSecret;
            })();

        return {
            ...result,
            secret,
        };
    });
};

// Note: for cases like https://www.w3schools.com/jsref/tryit.asp?filename=tryjsref_onblur
// There is a kind of strange refresh in the iframe on right hand side,
// while content script is not reloaded in the mean time, it causes that iframe not able to be recorded
// So we have to listen to url change in iframes for this case.
// frame发生了页面刷新，重新绑定事件
const onUrlChange = (function () {
    let callback = () => { };
    let lastUrl = window.location.href;
    const check = () => {
        if (window.location.href !== lastUrl) {
            log('url changed', lastUrl, window.location.href);
            lastUrl = window.location.href;
            callback();
        }
    };

    if (window.top === window) {
        return () => { };
    }

    setInterval(check, 2000);

    return (fn: any) => {
        callback = fn;
    };
})();

// 加载配置
const loadConfig = async () => {
    const config = await storage.get('config') || {}
    state.config = config;
    // IMPORTANT: broadcast status change to all frames inside
    broadcastToAllFrames('UPDATE_CONFIG', config);
    return config;
};

//  lw:执行后，给网页发送消息
// 或者触发自定义事件的方式
const invokeResponse = (data: any) => {
    window.postMessage(
        {
            cmd: 'INVOKE_RESPONSE',
            data,
        },
        '*'
    );
};

// 初始化
const init = () => {
    console.log('init >> start')
    unbindEventsToRecord(); // 卸载录制时绑定的事件
    bindEventsToRecord(); // 绑定录制时的事件

    bindEventsToInspect(); // 绑定捕获元素事件
    bindStorageListener(); // 绑定storage事件
    bindOnMessage(); // 绑定监听消息事件

    toggleBodyMark(true); // 标记插件已安装

    loadConfig().then((config) => removeBodyMarkIfNecessary(config)); // 根据配置移除插件标志

    // Note: only bind ipc events if it's the top window
    if (window.top === window) {
        // 顶层window中监听IPC通信
        // 顶层window中监听自定义的事件
        bindIPCListener();
        bindInvokeEvent();
    } else {
        // frame中监听url的改变
        onUrlChange(init);
    }
};

init();
